跳到主要内容

反射效率讨论

使用反射来创建一个类的实例的效率,肯定是要比直接使用 new 低的,那么到底低多少呢?下面我们来测试一下。我们使用 JMH(Java Microbenchmark Harness) 来进行基准测试,然后对比测试结果。

效率对比

实例创建效率对比

测试代码如下:

@Fork(1)
@BenchmarkMode(Mode.Throughput)
public class ReflectionTest01 {

@Benchmark
public void createDirect(Blackhole bh) {
User user = new User();
bh.consume(user);
}

@Benchmark
public void createReflection(Blackhole bh) throws Exception {
Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
Constructor<User> constructor = (Constructor<User>) clazz.getConstructor();
Object obj = constructor.newInstance();
bh.consume(obj);
}
}
java

测试结果:

Benchmark                           Mode  Cnt          Score          Error  Units
ReflectionTest01.createDirect thrpt 5 432226002.753 ± 62148903.219 ops/s
ReflectionTest01.createReflection thrpt 5 2367241.614 ± 22070.486 ops/s
text

下面这段代码

Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
Constructor<User> constructor = (Constructor<User>) clazz.getConstructor();
java

是为了获取类的构造函数,然后最终通过构造函数来创建对象。为了保证测试的准确性,我们仅考虑对象创建的差异。因此将此部分提取到方法外。然后再进行测试。完整代码如下:

@State(Scope.Benchmark)
@Fork(1)
@BenchmarkMode(Mode.Throughput)
public class ReflectionTest02 {

private Constructor<User> constructor;

public ReflectionTest02() {
try {
Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
constructor = (Constructor<User>) clazz.getConstructor();
} catch (Exception e) {
e.printStackTrace();
}
}


@Benchmark
public void createDirect(Blackhole bh) {
User user = new User();
bh.consume(user);
}

@Benchmark
public void createReflection(Blackhole bh) throws Exception {
Object obj = constructor.newInstance();
bh.consume(obj);
}
}
java

测试结果:

Benchmark                           Mode  Cnt          Score          Error  Units
ReflectionTest02.createDirect thrpt 5 437689219.856 ± 52931283.013 ops/s
ReflectionTest02.createReflection thrpt 5 170329396.922 ± 5503310.701 ops/s
text

可以看出有了极大的提升,但是依然没有直接创建对象快。我认为这个才是反射的效率与直接调用的效率对比。性能有2~3倍的差异,但是差异并不是特别大。

方法调用效率对比

@Fork(1)
@BenchmarkMode(Mode.Throughput)
public class ReflectionTest3 {

@Benchmark
public void testDirect(Blackhole bh) {
User user = new User();
user.setName("张三");
bh.consume(user.getName());
}

@Benchmark
public void testReflection(Blackhole bh) throws Exception {
User user = new User();
Field field = User.class.getDeclaredField("name");
field.setAccessible(true);
field.set(user, "张三");
bh.consume(field.get(user));
}

}
java

测试结果:

Benchmark                        Mode  Cnt           Score           Error  Units
ReflectionTest3.testDirect thrpt 5 2569734655.736 ± 230142819.512 ops/s
ReflectionTest3.testReflection thrpt 5 47565197.953 ± 4258351.454 ops/s
text

现在将字段获取放到方法外,再进行测试

@State(Scope.Benchmark)
@Fork(1)
@BenchmarkMode(Mode.Throughput)
public class ReflectionTest4 {

private final Field field;

public ReflectionTest4() {
try {
field = User.class.getDeclaredField("name");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}

@Benchmark
public void testDirect(Blackhole bh) {
User user = new User();
user.setName("张三");
bh.consume(user.getName());
}

@Benchmark
public void testReflection(Blackhole bh) throws Exception {
User user = new User();
field.setAccessible(true);
field.set(user, "张三");
bh.consume(field.get(user));
}

}
java

测试结果:

Benchmark                        Mode  Cnt           Score           Error  Units
ReflectionTest4.testDirect thrpt 5 2576261981.192 ± 307117964.204 ops/s
ReflectionTest4.testReflection thrpt 5 82960500.304 ± 3457384.884 ops/s
text

可以看到虽然有了很明显的提升,但是差距依然很大。

为什么反射的效率慢

在上面的代码中对象创建有 2~3 倍的差异,而方法的调用有30多倍的差异。

官方说明:

Performance Overhead

Because reflection involves types that are dynamically resolved, certain Java virtual machine optimizations can not be performed. Consequently, reflective operations have slower performance than their non-reflective counterparts, and should be avoided in sections of code which are called frequently in performance-sensitive applications.

由于反射涉及动态解析的类型,因此无法执行某些 Java 虚拟机优化。因此,反射操作的性能比非反射操作的性能慢,在性能敏感型应用程序中经常调用的代码段中应避免使用反射操作。

除了上面官方提到的一个原因外还有其他的原因。

  • 在获取类、构造函数、字段、方法等时,会进行额外的安全检查。
  • 所有操作都需要先进行查找或发现。如根据类的完全限定名获取类,根据方法名获取方法等。
  • 参数需要通过装箱/拆箱操作,参数会被打包成数组。
  • 异常信息回包裹在 InvocationTargetException 中重新抛出。
  • JIT 编译器执行的最强大的转换之一是自动方法内联。不幸的是,由于反射性调用的动态性质,它们在一般情况下通常不会内联。
注意

强制类型转换的性能开销可以忽略不计。有的文章中说反射中的强制类型转换会导致额外的性能开销,这种说法是不正确的。以下是一个强制类型转换和没有强制类型转换的性能测试结果:

Benchmark        Mode  Cnt          Score          Error  Units
CastTest.test1 thrpt 5 423612986.204 ± 45613060.042 ops/s
CastTest.test2 thrpt 5 428994015.602 ± 29943413.860 ops/s
text

上面是没有强制类型转换的结果,下面是有强制类型转换的结果。从结果中可以看出来基本没有差异。

此外并非所有反射方法都无法内联,有一些例外情况是可以被内联优化的。

The reflection implementation generates Java bytecode (using MethodAccessorGenerator.generateMethod()) for the call. This can make it easier for the JIT compiler to inline the reflective call if certain conditions hold, such as the Method object being rooted in a static final field and the target method being static (or having a definitely known receiver type).

也即如果反射的方法在一个 static final 字段上,并且这个方法是一个静态方法。是可能会被JIT编译器进行内联优化的。

总结

反射的功能很强大,但是性能慢,如果能避免使用它,最好避免使用。但也不能一棒子打死 “就是不能用反射”,反射在很多场景下还是非常实用的,特别是编写一些共用组件时。在 Spring 和 MyBatis 等一些开源框架中都大量的使用了反射。

反射还有一个问题,就是会暴露内部的字段或方法(如一些 private 字段或方法),使其可以突破原有访问限制而被访问,可能会造成一些麻烦。

相关资料